/*!
 *
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 *
 * Copyright (c) 2002-2018 Hitachi Vantara. All rights reserved.
 *
 */

package org.pentaho.platform.plugin.services.importexport.legacy;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.api.engine.IUserRoleListService;
import org.pentaho.platform.api.repository.RepositoryException;
import org.pentaho.platform.api.repository2.unified.IRepositoryFileData;
import org.pentaho.platform.api.repository2.unified.IUnifiedRepository;
import org.pentaho.platform.api.repository2.unified.MondrianSchemaAnnotator;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.api.repository2.unified.RepositoryFilePermission;
import org.pentaho.platform.api.repository2.unified.data.node.DataNode;
import org.pentaho.platform.api.repository2.unified.data.node.DataProperty;
import org.pentaho.platform.api.repository2.unified.data.node.NodeRepositoryFileData;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.engine.security.SecurityHelper;
import org.pentaho.platform.plugin.action.olap.IOlapServiceException;
import org.pentaho.platform.plugin.services.importexport.StreamConverter;
import org.pentaho.platform.repository2.ClientRepositoryPaths;
import org.pentaho.platform.repository2.unified.fileio.RepositoryFileInputStream;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;

import static org.pentaho.platform.repository.solution.filebased.MondrianVfs.ANNOTATIONS_XML;
import static org.pentaho.platform.repository.solution.filebased.MondrianVfs.ANNOTATOR_KEY;
import static org.pentaho.platform.repository.solution.filebased.MondrianVfs.SCHEMA_XML;


public class MondrianCatalogRepositoryHelper {

  public static final String ETC_MONDRIAN_JCR_FOLDER =
      ClientRepositoryPaths.getEtcFolderPath() + RepositoryFile.SEPARATOR + "mondrian";
  public static final String ETC_OLAP_SERVERS_JCR_FOLDER =
      ClientRepositoryPaths.getEtcFolderPath() + RepositoryFile.SEPARATOR + "olap-servers";
  private boolean isSecured = false;

  private static final Log logger = LogFactory.getLog( MondrianCatalogRepositoryHelper.class );

  private IUnifiedRepository repository;

  public MondrianCatalogRepositoryHelper( final IUnifiedRepository repository ) {
    if ( repository == null ) {
      throw new IllegalArgumentException();
    }
    this.repository = repository;
    try {
      if ( PentahoSystem.get( IUserRoleListService.class ) != null ) {
        isSecured = true;
      }
    } catch ( Throwable t ) {
      // That's ok. The API throws an exception and there is no method to check
      // if security is on or off.
    }
    initOlapServersFolder();
  }

  @Deprecated
  public void addSchema( InputStream mondrianFile, String catalogName, String datasourceInfo ) throws Exception {
    this.addHostedCatalog( mondrianFile, catalogName, datasourceInfo );
  }

  public void addHostedCatalog( InputStream mondrianFile, String catalogName, String datasourceInfo ) throws Exception {
    RepositoryFile catalog = createCatalog( catalogName, datasourceInfo );

    File tempFile = File.createTempFile( "tempFile", null );
    tempFile.deleteOnExit();
    FileOutputStream outputStream = new FileOutputStream( tempFile );
    IOUtils.copy( mondrianFile, outputStream );

    RepositoryFile repoFile = new RepositoryFile.Builder( "schema.xml" ).build();
    org.pentaho.platform.plugin.services.importexport.RepositoryFileBundle repoFileBundle =
        new org.pentaho.platform.plugin.services.importexport.RepositoryFileBundle( repoFile, null,
            ETC_MONDRIAN_JCR_FOLDER + RepositoryFile.SEPARATOR + catalogName + RepositoryFile.SEPARATOR, tempFile,
            "UTF-8", "text/xml" );

    RepositoryFile schema =
        repository.getFile( ETC_MONDRIAN_JCR_FOLDER + RepositoryFile.SEPARATOR + catalogName + RepositoryFile.SEPARATOR
            + "schema.xml" );
    IRepositoryFileData data =
        new StreamConverter().convert(
            repoFileBundle.getInputStream(), repoFileBundle.getCharset(), repoFileBundle.getMimeType() );
    if ( schema == null ) {
      RepositoryFile schemaFile = repository.createFile( catalog.getId(), repoFileBundle.getFile(), data, null );
      if ( schemaFile != null ) {
        // make sure the folder is not set to hidden if the schema is not hidden
        RepositoryFile catalogFolder =
          repository.getFile( ETC_MONDRIAN_JCR_FOLDER + RepositoryFile.SEPARATOR + catalogName );
        if ( catalogFolder.isHidden() != schemaFile.isHidden() ) {
          RepositoryFile unhiddenFolder =
            ( new RepositoryFile.Builder( catalogFolder ) ).hidden( schemaFile.isHidden() ).build();
          repository.updateFolder( unhiddenFolder, "" );
        }
      }
    } else {
      repository.updateFile( schema, data, null );
    }
  }

  public void deleteCatalog( String catalogName ) {
    deleteHostedCatalog( catalogName );
    deleteOlap4jServer( catalogName );
  }

  public void deleteHostedCatalog( String catalogName ) {

    final RepositoryFile catalogNode =
        repository.getFile(
            ETC_MONDRIAN_JCR_FOLDER
                + RepositoryFile.SEPARATOR
                + catalogName
      );

    if ( catalogNode != null ) {
      repository.deleteFile(
          catalogNode.getId(), true,
          "Deleting hosted catalog: "
              + catalogName
      );
    }
  }

  private void initOlapServersFolder() {
    final RepositoryFile etcOlapServers =
        repository.getFile( ETC_OLAP_SERVERS_JCR_FOLDER );
    if ( etcOlapServers == null ) {
      final Callable<Void> callable = new Callable<Void>() {
        public Void call() throws Exception {
          repository.createFolder(
              repository.getFile( RepositoryFile.SEPARATOR + "etc" ).getId(),
              new RepositoryFile.Builder( "olap-servers" )
                  .folder( true )
                  .build(),
              "Creating olap-servers directory in /etc"
          );
          return null;
        }
      };
      try {
        if ( isSecured ) {
          SecurityHelper.getInstance().runAsSystem( callable );
        } else {
          callable.call();
        }
      } catch ( Exception e ) {
        throw new RuntimeException(
            "Failed to create folder /etc/olap-servers in the repository.",
            e );
      }
    }
  }

  public void addOlap4jServer(
      String name,
      String className,
      String URL,
      String user,
      String password,
      Properties props ) {

    final RepositoryFile etcOlapServers =
        repository.getFile( ETC_OLAP_SERVERS_JCR_FOLDER );

    RepositoryFile entry =
        repository.getFile(
            ETC_OLAP_SERVERS_JCR_FOLDER
                + RepositoryFile.SEPARATOR
                + name
      );

    if ( entry == null ) {
      entry =
          repository.createFolder(
              etcOlapServers.getId(),
              new RepositoryFile.Builder( name )
                  .folder( true )
                  .build(),
              "Creating entry for olap server: "
                  + name
                  + " into folder "
                  + ETC_OLAP_SERVERS_JCR_FOLDER
        );
    }

    final String path =
        ETC_OLAP_SERVERS_JCR_FOLDER
            + RepositoryFile.SEPARATOR
            + name
            + RepositoryFile.SEPARATOR
            + "metadata";

    // Convert the properties to a serializable XML format.
    final String xmlProperties;
    final ByteArrayOutputStream os = new ByteArrayOutputStream();
    try {
      props.storeToXML(
          os,
          "Connection properties for server: " + name,
          "UTF-8" );
      xmlProperties =
          os.toString( "UTF-8" );
    } catch ( IOException e ) {
      // Very bad. Just throw.
      throw new RuntimeException( e );
    } finally {
      try {
        os.close();
      } catch ( IOException e ) {
        // Don't care. Just cleaning up.
      }
    }

    final DataNode node = new DataNode( "server" );
    node.setProperty( "name", name );
    node.setProperty( "className", className );
    node.setProperty( "URL", URL );
    node.setProperty( "user", user );
    node.setProperty( "password", password );
    node.setProperty( "properties", xmlProperties );
    NodeRepositoryFileData data = new NodeRepositoryFileData( node );

    final RepositoryFile metadata = repository.getFile( path );

    if ( metadata == null ) {
      repository.createFile(
          entry.getId(),
          new RepositoryFile.Builder( "metadata" ).build(),
          data,
          "Creating olap-server metadata for server "
              + name
      );
    } else {
      repository.updateFile(
          metadata,
          data,
          "Updating olap-server metadata for server "
              + name
      );
    }
  }

  public void deleteOlap4jServer( String name ) {
    // Get the /etc/olap-servers/[name] folder.
    final RepositoryFile serverNode =
        repository.getFile(
            ETC_OLAP_SERVERS_JCR_FOLDER
                + RepositoryFile.SEPARATOR
                + name
      );

    if ( serverNode != null ) {
      repository.deleteFile(
          serverNode.getId(), true,
          "Deleting olap server: "
              + name
      );
    }
  }

  /**
   * Provides a list of the catalog names which are not hosted on this server.
   * (generic olap4j connections)
   */
  public List<String> getOlap4jServers() {
    final RepositoryFile hostedFolder =
        repository.getFile( ETC_OLAP_SERVERS_JCR_FOLDER );

    if ( hostedFolder == null ) {
      // This can happen on old systems when this code first kicks in.
      // The folder gets created in addOlap4jServer
      return Collections.emptyList();
    }

    final List<String> names = new ArrayList<String>();

    for ( RepositoryFile repoFile : repository.getChildren( hostedFolder.getId() ) ) {
      names.add( repoFile.getName() );
    }

    return names;
  }

  public Olap4jServerInfo getOlap4jServerInfo( String name ) {
    final RepositoryFile serverNode =
        repository.getFile(
            ETC_OLAP_SERVERS_JCR_FOLDER
                + RepositoryFile.SEPARATOR
                + name
                + RepositoryFile.SEPARATOR
                + "metadata"
      );

    if ( serverNode != null ) {
      return new Olap4jServerInfo( serverNode );
    } else {
      return null;
    }
  }

  /**
   * Provides a list of the catalog names hosted locally on this server.
   */
  public List<String> getHostedCatalogs() {
    final List<String> names = new ArrayList<String>();

    final RepositoryFile serversFolder =
        repository.getFile( ETC_MONDRIAN_JCR_FOLDER );

    if ( serversFolder != null ) {
      for ( RepositoryFile repoFile : repository.getChildren( serversFolder.getId() ) ) {
        names.add( repoFile.getName() );
      }
    }
    return names;
  }

  public HostedCatalogInfo getHostedCatalogInfo( String name ) {
    final RepositoryFile catalogNode =
        repository.getFile(
            ETC_MONDRIAN_JCR_FOLDER
                + RepositoryFile.SEPARATOR
                + name
                + RepositoryFile.SEPARATOR
                + "metadata"
      );

    if ( catalogNode != null ) {
      return new HostedCatalogInfo( name, catalogNode );
    } else {
      return null;
    }
  }

  /**
   * Provides information on a catalog that the server hosts locally.
   */
  public final class HostedCatalogInfo {
    public final String name;
    public final String dataSourceInfo;
    public final String definition;

    public HostedCatalogInfo(
        String name,
        RepositoryFile source ) {
      final NodeRepositoryFileData data =
          repository.getDataForRead(
              source.getId(),
              NodeRepositoryFileData.class );
      this.name = name;
      this.dataSourceInfo =
          data.getNode().getProperty( "datasourceInfo" ).getString();
      this.definition =
          data.getNode().getProperty( "definition" ).getString();
    }

    public HostedCatalogInfo(
        String name,
        String dataSourceInfo,
        String definition ) {
      this.name = name;
      this.dataSourceInfo = dataSourceInfo;
      this.definition = definition;
    }
  }

  public final class Olap4jServerInfo {
    public final String name;
    public final String className;
    public final String URL;
    public final String user;
    public final String password;
    public final Properties properties;

    private Olap4jServerInfo( RepositoryFile source ) {

      final NodeRepositoryFileData data =
          repository.getDataForRead(
              source.getId(),
              NodeRepositoryFileData.class );

      this.name = data.getNode().getProperty( "name" ).getString();
      this.className = data.getNode().getProperty( "className" ).getString();
      this.URL = data.getNode().getProperty( "URL" ).getString();

      final DataProperty userProp = data.getNode().getProperty( "user" );
      this.user = userProp == null ? null : userProp.getString();

      final DataProperty passwordProp = data.getNode().getProperty( "password" );
      this.password = passwordProp == null ? null : passwordProp.getString();

      this.properties = new Properties();

      final String propertiesXml =
          data.getNode().getProperty( "properties" ).getString();
      try {
        properties.loadFromXML(
            new ByteArrayInputStream(
                propertiesXml.getBytes( "UTF-8" ) )
        );
      } catch ( Exception e ) {
        // Very bad.
        throw new RuntimeException( e );
      }
    }
  }

  public Map<String, InputStream> getModrianSchemaFiles( String catalogName ) {
    Map<String, InputStream> values = new HashMap<String, InputStream>();

    RepositoryFile catalogFolder =
        repository.getFile( ETC_MONDRIAN_JCR_FOLDER + RepositoryFile.SEPARATOR + catalogName );

    if ( catalogFolder == null ) {
      logger.warn( "Catalog " + catalogName + " not found" );
      throw new RepositoryException( "Catalog " + catalogName + " not found" );
    }

    for ( RepositoryFile repoFile : repository.getChildren( catalogFolder.getId() ) ) {
      RepositoryFileInputStream is;
      if ( repoFile.getName().equals( "metadata" ) ) {
        continue;
      }
      try {
        is = new RepositoryFileInputStream( repoFile, repository );
      } catch ( FileNotFoundException e ) {
        throw new RepositoryException( e );
      }
      values.put( repoFile.getName(), is );
    }
    if ( values.containsKey( ANNOTATIONS_XML ) && values.containsKey( SCHEMA_XML ) ) {
      return includeAnnotatedSchema( values );
    }
    return values;
  }

  private Map<String, InputStream> includeAnnotatedSchema( final Map<String, InputStream> values ) {
    MondrianSchemaAnnotator annotator =
        PentahoSystem.get( MondrianSchemaAnnotator.class, ANNOTATOR_KEY, PentahoSessionHolder.getSession() );
    try {
      if ( annotator != null ) {
        byte[] schemaBytes = IOUtils.toByteArray( values.get( SCHEMA_XML ) );
        byte[] annotationBytes = IOUtils.toByteArray( values.get( ANNOTATIONS_XML ) );
        values.put( SCHEMA_XML, new ByteArrayInputStream( schemaBytes ) );
        values.put( ANNOTATIONS_XML, new ByteArrayInputStream( annotationBytes ) );
        values.put( "schema.annotated.xml",
            annotator.getInputStream(
              new ByteArrayInputStream( schemaBytes ), new ByteArrayInputStream( annotationBytes ) ) );
      }
    } catch ( IOException e ) {
      throw new RepositoryException( e );
    }
    return values;
  }

  public RepositoryFile getMondrianCatalogFile( String catalogName ) {
    return repository.getFile( ETC_MONDRIAN_JCR_FOLDER + RepositoryFile.SEPARATOR + catalogName );
  }

  /*
   * Creates "/etc/mondrian/<catalog>"
   */
  private RepositoryFile createCatalog( String catalogName, String datasourceInfo ) {

    /*
     * This is the default implementation. Use Schema name as defined in the mondrian.xml schema. Pending create
     * alternate implementation. Use catalog name.
     */

    RepositoryFile catalog = getMondrianCatalogFile( catalogName );
    if ( catalog == null ) {
      RepositoryFile etcMondrian = repository.getFile( ETC_MONDRIAN_JCR_FOLDER );
      catalog =
          repository.createFolder( etcMondrian.getId(), new RepositoryFile.Builder( catalogName ).folder( true )
              .build(), "" );
    }
    createDatasourceMetadata( catalog, datasourceInfo );
    return catalog;
  }

  /*
   * Creates "/etc/mondrian/<catalog>/metadata" and the connection nodes
   */
  private void createDatasourceMetadata( RepositoryFile catalog, String datasourceInfo ) {

    final String path =
        ETC_MONDRIAN_JCR_FOLDER + RepositoryFile.SEPARATOR + catalog.getName() + RepositoryFile.SEPARATOR + "metadata";
    RepositoryFile metadata = repository.getFile( path );

    String definition = "mondrian:/" + catalog.getName();
    DataNode node = new DataNode( "catalog" );
    node.setProperty( "definition", encodeUrl( definition ) );
    node.setProperty( "datasourceInfo", datasourceInfo );
    NodeRepositoryFileData data = new NodeRepositoryFileData( node );

    if ( metadata == null ) {
      repository.createFile( catalog.getId(), new RepositoryFile.Builder( "metadata" ).build(), data, null );
    } else {
      repository.updateFile( metadata, data, null );
    }
  }

  private String makeHostedPath( String name ) {
    return
        MondrianCatalogRepositoryHelper.ETC_MONDRIAN_JCR_FOLDER
            + RepositoryFile.SEPARATOR
            + name;
  }

  private String makeGenericPath( String name ) {
    return
        MondrianCatalogRepositoryHelper.ETC_OLAP_SERVERS_JCR_FOLDER
            + RepositoryFile.SEPARATOR
            + name;
  }

  private boolean isHosted( String name ) {
    if ( getHostedCatalogs().contains( name ) ) {
      return true;
    } else {
      return false;
    }
  }

  private String makePath( String catalogName ) {
    if ( isHosted( catalogName ) ) {
      return makeHostedPath( catalogName );
    } else {
      return makeGenericPath( catalogName );
    }
  }

  public boolean hasAccess(
      final String catalogName,
      final EnumSet<RepositoryFilePermission> perms,
      IPentahoSession session ) {

    if ( session == null ) {
      // No session is equivalent to root access.
      return true;
    }

    // If the connection doesn't exist yet and we're trying to create it,
    // we need to check the parent folder instead.
    final String path;
    if ( !getHostedCatalogs().contains( catalogName )
        && !getOlap4jServers().contains( catalogName )
        && perms.contains( RepositoryFilePermission.WRITE ) ) {
      path = isHosted( catalogName )
          ? ETC_MONDRIAN_JCR_FOLDER
          : ETC_OLAP_SERVERS_JCR_FOLDER;
    } else {
      path = makePath( catalogName );
    }

    final IPentahoSession origSession = PentahoSessionHolder.getSession();
    PentahoSessionHolder.setSession( session );
    try {
      return repository.hasAccess(
          path,
          perms );
    } catch ( Exception e ) {
      throw new IOlapServiceException( e );
    } finally {
      PentahoSessionHolder.setSession( origSession );
    }
  }

  /**
   * Ensure URLs are properly encoded to accommodate
   *
   * @param urlStr
   * @return
   */
  private String encodeUrl( String urlStr ) {
    // make sure catalog definition url is properly encoded
    // try to encode the url before use
    String protocol = urlStr.substring( 0, urlStr.indexOf( ":" ) + 1 );
    String datasourcePath = urlStr.substring( protocol.length() );
    String[] folders = datasourcePath.split( "/" );
    StringBuilder encodedPath = new StringBuilder( urlStr.length() * 2 );
    for ( int i = 0; i < folders.length; i++ ) {
      String pathPart;

      try {
        final Charset urlCharset = Charset.forName( "UTF-8" );
        pathPart = URLEncoder.encode( folders[ i ], urlCharset.name() );
      } catch ( UnsupportedEncodingException e ) {
        pathPart = folders[i];
      }

      encodedPath.append( pathPart );

      if ( i != folders.length - 1 || urlStr.endsWith( "/" ) ) {
        encodedPath.append( "/" );
      }
    }

    return protocol + encodedPath.toString();
  }
}
